# microbench: CPU instructions benchmark based on google benchmark framework
# Copyright (C) 2022 Wu Zhangjin <falcon@ruma.tech>

# Build and run microbench for target ARCH on target machine

# Usage:
#
# X86_64:
#     $ make
#
# Others:
#     $ cp test/x86_64.cc test/$(ARCH).cc
#     $ // customize the instructions in test/$(ARCH).cc, please refer to $(ARCH) ISA
#     $ make

# src: benchmark/test/$(TEST_BIN).cc
# bin: benchmark/build/test/$(TEST_BIN)

ARCH      ?= $(shell uname -m)

TEST_BIN  := $(ARCH)
TEST_NAME := $(ARCH)

ifneq ($(O),)
  OPTIMIZE_LEVEL := $(O)
else
  OPTIMIZE_LEVEL := 1
endif

BENCHMARK_CMAKELIST := test/CMakeLists.txt

BENCHMARK_GITHUB    := https://github.com/google/benchmark.git
BENCHMARK_GITEE     := https://gitee.com/mirrors/benchmark.git

GOOGLETEST_GITHUB   := https://github.com/google/googletest.git
GOOGLETEST_GITEE    := https://gitee.com/mirrors/googletest.git

# Use gitee mirror by default
BENCHMARK_MIRROR    := $(BENCHMARK_GITEE)
GOOGLETEST_MIRROR   := $(GOOGLETEST_GITEE)

ifeq ($(wildcard test/$(TEST_BIN).cc),)
  $(error Please add support for $(ARCH), please refer to test/x86_64.cc and add new test/$(TEST_BIN).cc, cpumodel and product variables should be customized too.)
endif

ifeq ($(ARCH),x86_64)
  cpumodel   := $$(grep -m1 -i 'model name' /proc/cpuinfo  | tr '\t' ' ' | tr -s ' ' | cut -d ' ' -f4- | tr -c '[a-zA-Z0-9\.]' '-' | tr -s '-' | sed -e 's/-$$//g' | cut -c1-60)
  product    := $$(cat /sys/class/dmi/id/product_name | cut -d ' ' -f1)
endif

ifeq ($(ARCH),riscv64)
  # FIXME: cpu freq not in /proc/cpuinfo, should get it from /sys/devices/system/cpu/cpu0/cpufreq/{base_frequency,scaling_min_freq,scaling_max_freq}
  cpumodel   := $$(grep -E -m3 -i 'uarch|isa|mmu' /proc/cpuinfo  | tr '\t' ' ' | tr -s ' ' | cut -d ':' -f2 | sed -e "s/^ //g"| tr -c '[a-zA-Z0-9\.]' '-' | tr -s '-' | sed -e 's/-$$//g' | cut -c1-60)
  product    := $$(dmesg | grep -i 'Machine model' | cut -d ':' -f2 | sed -e "s/^ //g" | tr ' ' -f2)

  # If the host os is not $(ARCH), it should be cross compiling
  ifneq ($(ARCH),$(shell uname -m))
    CROSS_COMPILE ?= riscv64-linux-gnu-
  endif
endif

ifneq ($(filter $(ARCH),armv7 armv7l aarch64),)
  cpumodel   := $$(grep -E "Features|architecture|Hardware" /proc/cpuinfo | sed -e "s/Hardware/aHardware/" -e "s/CPU architecture.*: /CPU architecture : ARMv/g"  | sort | uniq | tr '\t' ' ' | cut -d ':' -f2- | sed -e "s/^ //g" | tr -c '[a-zA-Z0-9\.]' '-' | tr -s '-' | sed -e 's/-$$//g' | cut -c1-60)
  product    := $$(dmesg | grep model | sed -e "s/.*model/model/g" | sed -e "s/.*://g" | sed -e "s/^ //g" | tr ' ' -f2)

  # If the host os is not $(ARCH), it should be cross compiling
  ifneq ($(ARCH),$(shell uname -m))
    CROSS_COMPILE ?= subst(subst($(ARCH),v7,),v7l,)-linux-gnu-linux-gnu-
  endif
endif

ifeq ($(ARCH),loongarch64)
  cpumodel   := $$(grep -E -m3 -i 'model name|features' /proc/cpuinfo  | tr '\t' ' ' | tr -s ' ' | cut -d ':' -f2 | sed -e "s/^ //g"| tr -c '[a-zA-Z0-9\.]' '-' | tr -s '-' | sed -e 's/-$$//g' | cut -c1-60)
  product    := $$(cat /sys/class/dmi/id/product_name | cut -d ' ' -f1)
endif

ifneq ($(wildcard benchmark/test/microbench.h),)
  ifeq ($(shell grep "define OPTIMIZE_LEVEL $(OPTIMIZE_LEVEL)" benchmark/test/microbench.h),)
    dummy := $(shell sed -i -e "s/define OPTIMIZE_LEVEL.*/define OPTIMIZE_LEVEL $(OPTIMIZE_LEVEL)/g" benchmark/test/microbench.h)
  endif
endif

CC            := gcc
CXX           := g++

CROSS_CC      ?= $(CROSS_COMPILE)$(CC)
CROSS_CXX     ?= $(CROSS_COMPILE)$(CXX)
STATIC_FLAGS  ?= -static -Wl,--whole-archive -lpthread -Wl,--no-whole-archive -lc

ifneq ($(CROSS_COMPILE),)
  CMAKE_CC    ?= $$(which $(CROSS_COMPILE)$(CC))
  CMAKE_CXX   ?= $$(which $(CROSS_COMPILE)$(CXX))
  EXTRA_FLAGS ?= $(STATIC_FLAGS)
  CMAKE_FIXUP := -DHAVE_GNU_POSIX_REGEX=0 -DHAVE_POSIX_REGEX=0 -DHAVE_STD_REGEX=0
else
  CMAKE_CC    ?= $(CC)
  CMAKE_CXX   ?= $(CXX)
endif

ifeq ($(STATIC),1)
  EXTRA_FLAGS ?= $(STATIC_FLAGS)
endif

# ref: benchmark/.travis.yml
# Fixup for: terminate called after throwing an instance of '__gnu_cxx::__concurrence_broadcast_error'
CMAKE_DEPS    ?= -DBENCHMARK_DOWNLOAD_DEPENDENCIES=on -DGOOGLETEST_PATH=$(CURDIR)/benchmark/build/third_party/googletest
CMAKE_FLAGS   ?= -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEPS) -DCMAKE_C_COMPILER=${CMAKE_CC} -DCMAKE_CXX_COMPILER=${CMAKE_CXX} -DCMAKE_C_FLAGS="$(EXTRA_FLAGS)" -DCMAKE_CXX_FLAGS="$(EXTRA_FLAGS)" $(CMAKE_FIXUP)

# FIXME: No cross compiling support currently
TOOL_LIST    ?= cmake col $(CROSS_CC) $(CROSS_CXX)
TOOL_MISS    ?= $(shell for t in $(TOOL_LIST); do which $${t} >/dev/null 2>&1 || echo $${t}; done)
TOOL_INSTALL ?= $(shell for t in $(TOOL_LIST); do which $${t} >/dev/null 2>&1 || echo $${t} | sed -e "s/\(.*\)-\([^-]*\)/\2-\1/g;s/^col$$/bsdmainutils/g"; done)
TOOL_FILES   := $(addprefix /usr/bin/,$(TOOL_LIST))
SUDO         := $(shell which sudo)

all: benchmark/build/test/$(TEST_BIN)
	benchmark/build/test/$(TEST_BIN)

env: $(TOOL_FILES)

$(TOOL_FILES):
	$(Q)ret=0; if [ -n "$(TOOL_INSTALL)" ]; then \
	  if which apt >/dev/null; then \
	    $(SUDO) apt update -y; \
	    $(SUDO) apt install -y --no-install-recommends $(TOOL_INSTALL) || false; \
	    ret=$$?; \
	  fi; \
	  if which pacman >/dev/null; then \
	    $(SUDO) pacman -Sy; \
	    $(SUDO) pacman -S --noconfirm $(TOOL_INSTALL) || false; \
	    ret=$$?; \
	  fi; \
	  if which dnf >/dev/null; then \
	    $(SUDO) dnf makecache; \
	    $(SUDO) dnf install -y $(TOOL_INSTALL) || false; \
	    ret=$$?; \
	  fi; \
	fi; \
	[ $$ret -eq 0 ] || (echo "Err: missing '$(TOOL_MISS)' and failed to install: '$(TOOL_INSTALL)'" && false)

benchmark/build/test/$(TEST_BIN): benchmark/build/third_party/googletest/src benchmark/test/$(TEST_BIN).dep $(TOOL_FILES)
	cd benchmark && cmake $(CMAKE_FLAGS) -S . -B "build"
	cd benchmark && cmake --build "build" --config Release --target $(TEST_BIN)

benchmark/test/$(TEST_BIN).dep: benchmark/test/$(TEST_BIN).cc benchmark/test/common.cc benchmark/test/microbench.h
	cd benchmark && \
	git checkout -- $(BENCHMARK_CMAKELIST) && \
	sed -i -e "/compile_benchmark_test(basic_test)/icompile_benchmark_test($(TEST_BIN))" $(BENCHMARK_CMAKELIST) && \
	sed -i -e "/compile_benchmark_test(basic_test)/iadd_test(NAME $(TEST_NAME) COMMAND $(TEST_BIN) --benchmark_min_time=0.01)\n" $(BENCHMARK_CMAKELIST)
	touch $@

benchmark/test/$(TEST_BIN).cc: test/$(TEST_BIN).cc
	cd benchmark/test && cp ../../test/$(TEST_BIN).cc $(TEST_BIN).cc

benchmark/test/common.cc: test/common.cc
	cd benchmark/test && cp ../../test/common.cc common.cc

benchmark/test/microbench.h: test/microbench.h
	cd benchmark/test && cp ../../test/microbench.h microbench.h
	grep "define OPTIMIZE_LEVEL $(OPTIMIZE_LEVEL)" $@ || \
	sed -i -e "s/define OPTIMIZE_LEVEL.*/define OPTIMIZE_LEVEL $(OPTIMIZE_LEVEL)/g" $@

benchmark/src:
	git clone $(BENCHMARK_MIRROR)

benchmark/build/third_party/googletest/src: benchmark/src $(TOOL_FILES)
	sed -i -e "s%$(GOOGLETEST_GITHUB)%$(GOOGLETEST_MIRROR)%g" benchmark/WORKSPACE
	sed -i -e "s%$(GOOGLETEST_GITHUB)%$(GOOGLETEST_MIRROR)%g" benchmark/cmake/GoogleTest.cmake.in
	cd benchmark && cmake -E make_directory "build"
	cd benchmark && cmake -E chdir "build" cmake $(CMAKE_FLAGS) ../

logging:
	@tmpfile=$$(mktemp); \
	which col >/dev/null && filter_cmd="col -bp" || filter_cmd="cat"; \
	TEST_PRODUCT=$(product); \
	TEST_CPUMODEL=$(cpumodel); \
	[ -z "$$TEST_PRODUCT" ] && TEST_PRODUCT=$(or $(PRODUCT),"unknown"); \
	[ -z "$$TEST_CPUMODEL" ] && TEST_PRODUCT=$(or $(CPUMODEL),"unknown"); \
	logfile=logs/$${TEST_PRODUCT}-$${TEST_CPUMODEL}-$(TEST_NAME)-$$(date +"%Y%m%d-%H%M%S")-O$(OPTIMIZE_LEVEL).log \
	&& echo "System: $$(uname -v)" >> $$tmpfile \
	&& echo "Gcc: $$(gcc --version | head -1)" >> $$tmpfile \
	&& echo "G++: $$(g++ --version | head -1)" >> $$tmpfile \
	&& make --no-print-directory 2>&1 | tee -a $$tmpfile \
	&& mkdir -p logs \
	&& cat $$tmpfile | $$filter_cmd > $$logfile && rm $$tmpfile

clean:
	rm -rf benchmark/build/test/$(TEST_BIN)
	rm -rf benchmark/test/$(TEST_BIN).cc
	rm -rf benchmark/test/$(TEST_BIN).dep
	cd benchmark && git checkout -- $(BENCHMARK_CMAKELIST)
	make --no-print-directory clean -C benchmark/build/third_party/googletest/build
	make --no-print-directory clean -C benchmark/build

distclean:
	rm -rf benchmark
